package edu.moravian.video;

import edu.moravian.exceptions.VideoConfigurationException;
import edu.moravian.game.Game;
import edu.moravian.math.Point2D;
import edu.moravian.utilities.Timer;
import edu.moravian.world.CoordinateTranslator;
import edu.moravian.world.World2D;
import java.awt.Cursor;
import java.awt.DisplayMode;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.MouseInfo;
import java.awt.Point;
import java.awt.Toolkit;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.awt.image.BufferStrategy;
import javax.swing.JFrame;

/**
 * @author mebjc01
 * @author ronaghan
 *
 * This class encapsulates a graphics loop for a full-screen game.
 *
 * The class implements Runnable, and is intended to be used as
 * a thread.
 *
 */
public class VideoController extends JFrame implements Runnable, MouseMotionListener, KeyListener
{
    // Convience references to the Graphics system
    private GraphicsEnvironment genv;
    private GraphicsDevice gdev;
    
    //a flag to indicate whether or not we should draw the fps and mouse
    private boolean drawFPS;
    private boolean displayMouse;
    
    
    // The parameters specified by the user
    private int width;
    private int height;
    private int depth;
    private Game game;
    
    //Used to time the length of a frame 
    private Timer timer;

    
    //The translator between the screen resolution and the game resolution
    CoordinateTranslator coordTrans;
    private double gameWidth;
    private double gameHeight;
    
    /**
     * Create an instance of the class with the specified screen
     * configuration using the given game.
     * 
     * @param width the width of the desired screen resolution
     * @param height the height of the desired screen resolution
     * @param depth the color depth of the desired screen resolution
     * @param theGame the game to use in the update loop
     * @param gameWidth the width of the game world
     * @param gameHeight the height of the game world
     * @throws VideoConfigurationException if the desired video mode is
     *         not available or if fullscreen mode is not allowed
     */
    public VideoController(int width, int height, int depth, Game theGame, double gameWidth, double gameHeight)
            throws VideoConfigurationException
    {
        this.width = width;
        this.height = height;
        this.depth = depth;
        this.game = theGame;
        this.gameWidth = gameWidth;
        this.gameHeight = gameHeight;
        
        
        //by default do not draw the fps 
        this.drawFPS = false;
        this.displayMouse = false;

        
        // Save references to the graphics environment and device
        // for future reference
        genv = GraphicsEnvironment.getLocalGraphicsEnvironment();
        gdev = genv.getDefaultScreenDevice();
        
        //ensure we can run the game at the speciifed resolution, in full screen mode
        if(!hasResolution(width, height, depth)) 
        {
            throw new VideoConfigurationException("unsupported resolution:");
        }

        if(!hasFullScreen())
        {
            throw new VideoConfigurationException("fullscreen unsupported");
        }
        
        // Full-screen applications should not be resizable, they
        // should not have decorations (title bar, etc.)
        setResizable(false);
        setUndecorated(true);

        // Because we are going to use active rendering (control the video
        // directly), our program should not respond to the normal
        // repaint requests.
        setIgnoreRepaint(true);
        
        
        //initialize the timer and set the coordinate translator
        timer = new Timer();
        coordTrans = new CoordinateTranslator(gameWidth, gameHeight, width, height);
        
        //add ourself as a mouse motion and key listener, we will then delegate the tasks
        this.addMouseMotionListener(this);
        this.addKeyListener(this);
    }

    /**
     * Determines whether fullscreen mode is available
     * @return true if fullscreen is available
     */
    private boolean hasFullScreen()
    {
        return gdev.isFullScreenSupported();
    }
    

    /**
     * Determines whether the specified resolution is available
     * @param width the desired width
     * @param height the desired height
     * @param bitDepth the desired color depth
     * @return true if the specifed resolution is available
     */
    private boolean hasResolution(int width, int height, int bitDepth)
    {
        DisplayMode[] modes = gdev.getDisplayModes();

        for(int i = 0; i < modes.length; i++)
        {
            int w = modes[i].getWidth();
            int h = modes[i].getHeight();
            int b = modes[i].getBitDepth();

            if(width == w && height == h && bitDepth == b)
                return true;
        }
        
        return false;
    }
    

    /**
     * This method is the process of the loop.  After changing to the
     * specified resolution, the method executes the game as an
     * update loop.  When the game indicates it is done, the loop
     * terminates.
     */
    public void run()
    {
        
        
        
        // Save the old resolution so we can go back when we are done.
        DisplayMode oldMode = gdev.getDisplayMode();

        // Many of the following method calls can cause exceptions
        // to be thrown.  If unhandled, and exception will cause the program
        // to terminate, and depending on the system, the video system may
        // be left in a "bad" state.  Thus, we enclose all our code in a huge
        // try block with a "finally" clause.  A "finally" clause will *always*
        // execute even if an unhandled exception causes the program to
        // terminate.  We use this to make sure we "clean up" the video

        try
        {
            // Go full screen!  "this" is the object, which is a window
            gdev.setFullScreenWindow(this);
            // Change to our desired resolution
            gdev.setDisplayMode(new DisplayMode(width, height, depth,
                    DisplayMode.REFRESH_RATE_UNKNOWN));

                        
            // We want our program to use two buffers for graphics - one
            // is the display on the screen, and the other is the one
            // we write to.  More than two buffers is possible, but not
            // needed for what we need.
            // NOTE:  This cannot be done until *after* we are fullscreen
            this.createBufferStrategy(2);

            // Now that the strategy is created, we safe a reference to it
            // so that we can draw (below)
            BufferStrategy bufStrat = this.getBufferStrategy();
            
                     
            //initial setup of mouse based on default dispay mouse value
            displayMouse(displayMouse);
            
            // Keep going until the game says it is done
            while(game.done() == false)
            {
                //mark the clock for this frame
                timer.tick();
                // Tell the game object to update itself - i.e. to make itself
                // ready for the next draw.
                game.update(timer.getDelta());

                // Drawing is done through a Graphics object.  You can think
                // of this as the object representing the screen.
                Graphics2D g2d = (Graphics2D)bufStrat.getDrawGraphics();

                //wrap the actual drawing canvas with our world 
                World2D w2d = new World2D(g2d, coordTrans);
                
                // Tell the game to draw itself using the graphics context
                game.draw(w2d);
                
                // Write the FPS in the upper-left corner. 
                if(drawFPS)
                {
                    int fontSize = 30;
                    Font oldFont = w2d.getFont();
                    Font newFont = new Font(oldFont.getName(), oldFont.getStyle(), fontSize);
                    w2d.setFont(newFont);
                    w2d.drawString(String.format("FPS: %.1f", timer.getFPS()), new Point2D(0.0, gameHeight));
                    w2d.setFont(oldFont);
                }
                
               
                // Free up any resources being used.
                w2d.dispose();

                
                // Now that we have drawn everything we want to see,
                // we "show" it, meaning tha the data in the 2nd buffer becomes
                // what is on the screen.
                bufStrat.show();

                // A monitor can only draw frames so fast.  If our frame rate
                // is faster than the monitor's refresh rate, frames will
                // either be dropped by the video system or they will be
                // drawn and cause video flicker.  To avoid this, we make
                // the program sleep.
                // If the OS forces the thread to wake up before the time,
                // then an exception will be thrown.  We can simply ignore
                // this exception and continue generating frames.
                try
                {
                    Thread.sleep(10);
                }
                catch (InterruptedException ex)
                {
                    // Do nothing
                }

            }

        }
        // A "finally" clause will always execute even if an unhandled
        // exception causes the program to terminate.  We use this to
        // make sure we "clean up" the video
        finally
        {
            
            //remove ourselves as event listeners
            this.removeMouseMotionListener(this);
            this.removeKeyListener(this);
            
            // Before we exit, we want to change back to the original
            // video mode.
            gdev.setDisplayMode(oldMode);
            // This command returns the screen to non-full-screen.
            gdev.setFullScreenWindow(null);
            
            //get rid of any held up resources
            this.dispose();
            
        }
    }

    
    
    
    /**
     * Helper method that changes the mouse depending on the display option
     * 
     * @param display whether or not to display the mouse cursor
     */
    private void displayMouse(boolean display)
    {
        if(display)
        {
            this.setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
        }
        else
        {
            Toolkit tk = Toolkit.getDefaultToolkit();
            this.setCursor(tk.createCustomCursor(tk.getImage(""), new Point(), "trans"));
        }
    }
    
    


    
    
    /**
     * Listen for key type events and either handle here or delegate to 
     * the game if the action isn't for us
     * 
     * @param e the key event 
     */
    @Override
    public void keyTyped(KeyEvent e) 
    {
        if(e.getKeyChar() == 'f')
        {
            drawFPS = !drawFPS;
        }
        else if( e.getKeyChar() == 'm')
        {
            displayMouse = !displayMouse;
            displayMouse(displayMouse);
        }
        else
        {
            if(e.getKeyChar() == ' ')
            {
                Point p = MouseInfo.getPointerInfo().getLocation();
                game.handleMouseMoved(coordTrans.screenPointToWorldPoint(p));    
            }
                
            game.handleKeyTyped(e);
           
        }
    }

    /**
     * Similar to key typed but all key presses are relevant to the game
     * 
     * @param e the keyboard event
     */
    @Override
    public void keyPressed(KeyEvent e) 
    {
        game.handleKeyPress(e);
    }

    
    /**
     * Similar to key typed but all key releases are relevant to the game
     * 
     * @param e they keyboard event
     */
    @Override
    public void keyReleased(KeyEvent e) 
    {
        game.handleKeyRelease(e);
    }
    
    
    
    @Override
    public void mouseDragged(MouseEvent e) 
    {
        //do nothing for state game
    }

    /**
     * Listens for mouse movement
     * 
     * @param e the mouse event
     */
    @Override
    public void mouseMoved(MouseEvent e)
    {
        //do nothing for state game
    }
    
    
}
